CPP|通过编译器行为来了解复杂的语法机制

再复杂的语法机制最终都需由编译器来实现,所以了解背后的编译器行为,能更深入理解一些语法机制的原理。

1 变量与内存地址

CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址 。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

程序中定义一个变量时,编译器会预留一个内存位置来储存变量。此位置有唯一的地址。编译器把该地址与变量名相关联。当程序使用该变量名时,将自动访问正确的内存位置。

编译器不为寄存器类型的变量分配内存空间,而是直接使用CPU的寄存器。以便提高对这类变量的存取速度。主要用于控制循环次数等不需要长期保存值得变量。

2 C风格字符串

在字符数组末尾包含一个表示字符串结束的空字符\0,这样该数组的内容就构成了一个字符串。所以严格意义上来说,字符数组不一定是字符串,而字符串一定是数组。

下面表达式中字符串常量都是由编译器在构造字符数组时,在数组末尾添加一个空字符\0:

char *cp;

cp = "abcdef";//编译器会自动把"abcdef"定义为常量指针

char carr[] = "abcdef";

3 对于结构体和类类型的数据成员的内存映像

编译器会自动对齐结构体或类类型数据成员以提高运行效率。

4 数组的下标访问

数组各元素在内存空间是连续线性存储的,数组名对应首元素的地址,其它元素也就是相对于首元素地址的偏移,使用下标符号[]来表示具体偏移量,首元素的偏移量是0。

采用a[i]这种形式访问数组,编译器总会把其“改写”成像*(a+i)这种指针访问。

new(delete)与malloc(free)的主要区别是除了分配与释放内存,还会自动调用构造函数与析构函数。对于数组动态内存的释放,需要使用如delete [] stringParr;

符号[]是告诉编译器,我这里需要释放的是一个整个数组的指针,这样编译器就会逐个释放每个元素的内存占用。

数组有有初始化时,数组的第一维的长度可以省略,编译器由初始化成员的数量和其他维的长度来推断。

下标运算符为什么要重载成成员函数?下标运算符重载函数为什么要用引用返回?

下标运算符的第一个运算数是数组名,即当前类的对象。将下标运算符重载成成员函数时,编译器会将程序中诸如a[i]的下标变量的引用改为a.operator[](i)。如果a不是当前类的对象,编译器就会报错。下标变量是左值,所以必须用引用返回。

5 静态和全局变量的初始化

编译器会自动初始化static静态和global全局变量,即data段的内容。将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为false,指针为NULL,等等,即.bss段的内容

对于局部变量,基于效率的考量,编译器并不会自动初始化。但对于超过作用域的局部变量,编译器会自动释放其栈空间的占用。

6 类型转换

6.1 隐式类型转换

赋值时的自动类型转换:当表达式的结果类型和变量类型不一致时,系统会将右边的表达式的结果转换成左边的变量的类型,再赋给左边的变量。

char c = 'A';

int i = c; //隐式类型转换

6.2 static_cast与dynamic_cast

static_cast与dynamic_cast:把这两个放在一起比较容易记忆,"一静一动"。从字面上也可以看出,前者提供的是编译时期的静态类型检测,后者提供的是运行时检测。

static_cast进行强制类型转换时,会进行编译时的类型安全检查,即你可以将int转成float,将子类引用(指针)转成父类引用(指针),子转父和父转子都可以,但父转子不安全。但是你不能使用static_cast将两个无关的东西进行转换,比如两个无关的类,因为编译器在编译的时候会检查这个转换是否可行,很明显不可行。

7 函数参数缺省

C++在说明函数原型时,可以为一个或多个参数指定缺省值。调用此函数时,若缺省某一参数,C++编译器会自动以缺省值作为此参数的值。

如:

int special(int x=2, float y=1.5)

调用时可用:

special(5,3.2) //x=5; y=3.2

special(6) //x=6; y=1.5

special( ) //x=2; y=1.5

8 内联函数

将函数指定为内联是建议编译器在调用点直接把函数代码展开。内联函数避免了调用函数的代价。

在内联函数内不允许用循环语句和switch语句。如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,

9 类型检查与判断

编译器会在编译期对变量进行类型检查,确保变量的合法性。

const 常量有数据类型,而宏常量没有数据类型。使用的时候,编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生一些意料不到的错误。

auto自动变量是较新的C++版本新增的类型,声明变量时,用auto代替变量类型,由编译器根据初始字面值来判断变量类型。

10 静态成员变量

类的静态成员变量在编译阶段就分配空间,对象还没有创建。

11 引用

引用一般都是通过指针来实现的,只不过编译器帮我们完成了转换。

引用的实质就是一个由编译器实现了解引用的特殊的指针常量。

12 成员函数的this指针

每个对象包含两部分内容:数据成员和成员函数。不同的对象有不同的数据成员值,因此每个对象都拥有一块保存自己数据成员值的空间。但同一类的所有对象的成员函数的实现都是相同的,因此所有的对象共享了一份成员函数的代码。这又带来了另一个问题:成员函数中的涉及到的数据成员到底是哪个对象的数据成员?为此C++让每个成员函数都包含了一个隐含的参数this,该参数是一个指针,指向当前调用该成员函数的对象。成员函数中涉及的数据成员都是this指针指向的对象的数据成员。

类的成员函数在编译时,会多出一个this 指针。至于类别之中,成员函数的定义:

class CShape

{

...

public:

void setcolor(int color)

{ m_color = color; }

};

//被编译器整治过后,其实是:

class CShape

{

...

public:

void setcolor(int color, (CShape*)this)

{ this->m_color = color; }

};

通常,在写成员函数时可以省略this,编译时会自动加上它们。

void Point::setx(double inputx)

{

x=inputx;

this->x = inputx;

(*this).x = inputx;

}

如果在成员函数中要把对象作为整体来访问时,必须显式地使用this指针。这种情况常出现在函数中返回一个对调用函数的对象的引用。

13 函数重载时的编译器行为

重载函数是通过“名字压延”方法来实现。即在编译时将函数名和参数结合起来创造一个新的函数名,用新的名字替换原有名字。

编译器根据函数声明中的不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。

例如,下面的代码有两个同名函数:

long func(int i);

long func(couble dd);

那么编译器做过修饰后的函数名称可能是这样的:int_func、double_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就已经绑定了(早绑定),因此,重载和多态是没有关系的。

14 运算符重载

对于基本数据类型,都有对应的一组操作符,你也可以理解为是适用于基本数据类型的一组函数。

对于类类型,也可以如同基本数据类型一样,定义一组操作符,让类型之间的操作看起来更简洁。

如以下复数类的加法操作,你可以定义为一个函数,也可以定义为操作符重载:

complex add(const complex &c1, const complex &c2)

{

complex temp(c1.real + c2.real, c1.imag + c2.imag);

return temp;

}

complex operator+(const complex& c1, const complex& c2)

{

complex temp(c1.real + c2.real, c1.imag + c2.imag);

return temp;

}

c1+c2会被编译器理解为:

complex::operator+(c1,c2){}

15 虚函数和运行期多态

虚函数前的virtual就是告诉编译器,函数不是使用静态绑定(静态只能实现一对一,也就只能绑定一个),而是要使用动态绑定。

运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,告诉编译器在在编译时不得绑定,编译器将为该类对象安插一个虚函数表指针,并为类(基类和派生类)设置虚函数表,虚函数表中存放的是该类虚函数地址。同时,改写虚函数调用代码(函数指针调用)。运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。具体细节见:

C++运行期多态和编译期多态(以不同的模板参数调用不同的函数)

16 空类,定义时会生成6个成员函数

当空类Empty定义一个对象_oneEmpty pt;

sizeof(pt)仍是为1,但编译器会生成6个成员函数:一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、两个取址运算符。

class Empty

{};

等价于:

class Empty

{

public:

Empty(); //缺省构造函数

Empty(const Empty &rhs); //拷贝构造函数

~Empty(); //析构函数

Empty& operator=(const Empty &rhs); //赋值运算符

Empty* operator&(); //取址运算符

const Empty* operator&() const; //取址运算符(const版本)

};

使用时的调用情况:

Empty *e = new Empty(); //缺省构造函数

delete e; //析构函数

Empty e1; //缺省构造函数

Empty e2(e1); //拷贝构造函数

e2 = e1; //赋值运算符

Empty *pe1 = &e1; //取址运算符(非const)

const Empty *pe2 = &e2; //取址运算符(const)

17 其它

在派生类中用override修饰符定义虚函数时,编译器会验证直接或间接基类是否包含签名相同的虚函数,如果不包含,就生成一个错误信息。

如果类的函数成员用final修饰符指定,派生类就能重写该函数。否则编译器会生成一个错误消息。

如果类用final修饰符定义,该类就不能用作另一个类的基类。尝试把final类用作基类,编译器会生成一个错误消息。

-End-

本页共135段,4615个字符,10764 Byte(字节)